﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;

namespace SimpleRest.Serialization
{
    /// <summary>
    /// Rethink caching mechaism and breaking this class down into caching and helper methods.
    /// </summary>
    public static class TypeCache
    {
        private static System.Collections.Hashtable Items { get; set; }
        public readonly static List<Type> PrimitiveTypes;
        static TypeCache()
        {
            PrimitiveTypes = new List<Type>()
            {
                typeof(Int16),
                typeof(Int32),
                typeof(Int64),
                typeof(double),
                typeof(float),
                typeof(char),
                typeof(Guid),
                typeof(string),
                typeof(byte),
                typeof(UInt16),
                typeof(UInt32),
                typeof(UInt64),
                typeof(DateTime),
                typeof(bool),
                typeof(decimal)
            };

            Items = new System.Collections.Hashtable();
        }

        public static object GetPrimitiveSampleObject(Type primitiveType)
        {
            if (!IsPrimiviteType(primitiveType, true))
                throw new Exception("A primitive type must be supplied.");

            object obj = null;
            bool isNullableType = IsNullableType(primitiveType);
            if (isNullableType)
                primitiveType = primitiveType.GetGenericArguments()[0];

            if (primitiveType.FullName == typeof(Int16).FullName)
                obj = Int16.MaxValue;
            else if (primitiveType.FullName == typeof(Int32).FullName)
                obj = Int32.MaxValue;
            else if (primitiveType.FullName == typeof(Int64).FullName)
                obj = Int64.MaxValue;
            else if (primitiveType.FullName == typeof(double).FullName)
                obj = double.MaxValue;
            else if (primitiveType.FullName == typeof(float).FullName)
                obj = float.MaxValue;
            else if (primitiveType.FullName == typeof(char).FullName)
                obj = 'A';
            else if (primitiveType.FullName == typeof(Guid).FullName)
                obj = Guid.NewGuid();
            else if (primitiveType.FullName == typeof(string).FullName)
                obj = "String Value";
            else if (primitiveType.FullName == typeof(byte).FullName)
                obj = byte.Parse("a");
            else if (primitiveType.FullName == typeof(UInt16).FullName)
                obj = UInt16.MaxValue;
            else if (primitiveType.FullName == typeof(UInt32).FullName)
                obj = UInt32.MaxValue;
            else if (primitiveType.FullName == typeof(UInt64).FullName)
                obj = UInt64.MaxValue;
            else if (primitiveType.FullName == typeof(DateTime).FullName)
                obj = DateTime.Now;
            else if (primitiveType.FullName == typeof(bool).FullName)
                obj = false;
            else if (primitiveType.FullName == typeof(decimal).FullName)
                obj = decimal.MaxValue;
            else
                throw new Exception("Primitive type not supported.");

            if (isNullableType)
            {
                var item = Activator.CreateInstance(primitiveType);
                item = obj;
                return item;
            }
            return obj;
        }

        public static object GetHydratedSampleObject(Type type, List<Type> typesEvaluated = null, Type parentType = null)
        {
            if (typesEvaluated == null) typesEvaluated = new List<Type>();

            if (typesEvaluated.Count(d => d.FullName == type.FullName) > 1)
                return null;

            if (IsPrimiviteType(type, true))
            {
                typesEvaluated.Add(type);
                return GetPrimitiveSampleObject(type);
            } 
            else if (IsCollectionType(type))
            {
                object obj = Activator.CreateInstance(type);
                var arguments = type.GetGenericArguments();
                if ((arguments == null || arguments.Length == 0) && type.BaseType != null)
                    arguments = type.BaseType.GetGenericArguments();
                Type targetType = arguments.First();

                if (arguments != null && arguments.Length == 1)
                {
                    var add = type.GetMethod("Add");

                    if (IsPrimiviteType(targetType))
                        add.Invoke(obj, new[] { GetPrimitiveSampleObject(targetType) });
                    else
                    {
                        typesEvaluated.Add(targetType);
                        add.Invoke(obj, new[] { GetHydratedSampleObject(targetType, typesEvaluated, type) });
                    }
                }
                else if (arguments != null && arguments.Length > 1)
                    throw new Exception("Unable to build samples for lists of multiple types.");

                return obj; // TODO: Solve for edge cases like array's, IEnumerable, etc...
            }
            else
            {
                object obj = Activator.CreateInstance(type);

                foreach (var property in GetProperties(type))
                {
                    if (IsPrimiviteType(property.PropertyType))
                    {
                        property.SetValue(obj, GetPrimitiveSampleObject(property.PropertyType), null);
                    }
                    else
                    {
                        typesEvaluated.Add(property.PropertyType);
                        property.SetValue(obj, GetHydratedSampleObject(property.PropertyType, typesEvaluated, type), null);
                    }
                }

                return obj;
            }
        }

        public static PropertyInfo GetTypeProperty(Type type, string propertyName)
        {
            TypeCacheItem item = null;
            if (Items.ContainsKey(type))
            {
                item = Items[type] as TypeCacheItem;
            }
            else
                item = CacheType(type);

            return item.AssociatedProperties[propertyName.ToLower()] as PropertyInfo;
        }

        public static TypeCacheItem CacheType(Type type)
        {
            if (Items.ContainsKey(type))
                return Items[type] as TypeCacheItem;

            TypeCacheItem item = new TypeCacheItem();
            Items.Add(type, item);
            item.IsCollection = IsCollectionType(type);
            item.IsPrimitive = IsPrimiviteType(type);
            item.Target = type;
            item.AssociatedProperties = new System.Collections.Hashtable();
            item.AssociatedAttributes = new System.Collections.Hashtable();

            foreach (var property in type.GetProperties())
                item.AssociatedProperties.Add(property.Name.ToLower(), property);
            foreach (var attribute in type.GetCustomAttributes(true))
                item.AssociatedAttributes.Add(attribute.GetType(), attribute);

            return item;
        }

        public static TypeCacheItem GetType(object obj)
        {
            if (obj == null)
                return null;
            Type type = obj.GetType();
            if (!Items.ContainsKey(type))
                CacheType(type);

            return Items[type] as TypeCacheItem;
        }

        public static void Add(TypeCacheItem item)
        {
            Items.Add(item.Target, item);
        }

        public static IEnumerable<PropertyInfo> GetProperties(Type type)
        {
            TypeCacheItem item = null;
            if (!Items.ContainsKey(type))
                item = CacheType(type);
            else
                item = Items[type] as TypeCacheItem;

            return item.AssociatedProperties.Values.Cast<PropertyInfo>().OrderBy(d => IsCollectionType(d.PropertyType));
        }

        public static bool IsNullableType(Type type)
        {
            return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
        }
        public static Type GetNullableType(Type type)
        {
            if (!IsNullableType(type))
                return null;
            return type.GetGenericArguments()[0];
        }

        public static bool IsPrimiviteType(Type type, bool isNullableAPrimitive = true)
        {
            if (!Items.ContainsKey(type))
                CacheType(type);

            if (type.IsGenericType && isNullableAPrimitive && type.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                type = type.GetGenericArguments()[0];
            }

            return type.IsPrimitive || PrimitiveTypes.Contains(type) || type.IsEnum;
        }

        public static bool IsCollectionType(Type type)
        {
            if (!Items.ContainsKey(type))
                CacheType(type);

            if (type.IsArray)
                return true;

            var enumType = type.GetInterface(typeof(System.Collections.IEnumerable).FullName);
            if (enumType != null)
            {
                var args = type.GetGenericArguments();
                if (args != null && args.Length > 1)
                    throw new Exception("Cannot represent an array of more than one type.");

                return true;
            }

            return type.BaseType == null ? false : IsCollectionType(type.BaseType);
        }

        public static T GetAttribute<T>(Type type) where T : Attribute
        {
            if (!Items.ContainsKey(type))
                CacheType(type);

            Type targetType = typeof(T);
            var item = (Items[type] as TypeCacheItem);
            if (item.AssociatedAttributes == null || !item.AssociatedAttributes.ContainsKey(targetType))
                return null;
            else
                return (T)Convert.ChangeType(item.AssociatedAttributes[targetType], targetType);
        }

        public static Type GetGenericArgumentForCollection(Type type)
        {
            var arguments = type.GetGenericArguments();
            if (arguments.Length == 0 && type.BaseType != null)
            {
                return GetGenericArgumentForCollection(type.BaseType);
            }
            else if (arguments.Length > 1)
                throw new Exception("Serializer will only allow for one generic type for collections.");
            else if (arguments.Length == 1)
                return arguments[0];

            return null;
        }
    }
}
